#/usr/bin/python
# -*- encoding=UTF-8 -*-


import re
import os
import time
import telnetlib
import types
from ftplib import FTP

CRNC_UNIT = ['ICSU', 'OMU']

class BaseCrnc:
    def __init__(self, name, info, recorder=None, logger=None):
        self.name = name
        self.info = info
        self.logger = logger
        self.recorder = recorder
        self.connected = False
        self.tn = None
        self.prompt = '< '
        self.timestamp = 0
        self.unit_info = None
        self.package = None
        self.build = None
        self.corr = None
        self.allout = None
        self.stdout = None
        self.stderr = 0
        self.stdout_lastline = None

    def __del__(self):
        self.close()

    def close(self):
        self.connected = False
        if self.tn:
            self.record('\n')
            try:
                self.tn.close()
            except Exception:
                pass

    def log(self, level, msg, **kwargs):
        """recall the self.logger(which is the calss RecordLogger), 
        and add the head parameter.
        @level: values(notice|debug|info|warn|error|exception)
        """
        if self.logger and hasattr(self.logger, level):
            if not kwargs.has_key('head'):
                kwargs['head'] = self.name
            getattr(self.logger, level)(msg, **kwargs)
        # to delete
        else:
            print "self.logger has not attr %s" % level

    def record(self, msg, func_name='write'):
        """re-call self.recorder(which is the calss RecordFile)
        @func_name: value(write|write_file|record)
               the call function for self.recorder, 
        """
        if self.recorder and hasattr(self.recorder, func_name):
            getattr(self.recorder, func_name)(msg)
        # to delete
        else:
            print "self.recorder has not fucntion %s" % func_name

    def set_prompt(self, prompt):
        self.prompt = prompt

    def connect(self, timeout=10.0):
        self.connected = False
        msg = "Telnet host %s(%s)" % (self.name, self.info.hostname)
        self.log("info", msg, end=' ')
        try:
            self.tn = telnetlib.Telnet(self.info.hostname)
        except Exception:
            self.log('notice', "failed", head='', tag='r')
            self.stderr = "Telnet host exception"
            self.log("exception", self.stderr)
            return False

        #self.tn.set_debuglevel(2)

        if self.expect('ENTER USERNAME < ', std=False) < 0:
            self.log('notice', "failed", head='', tag='r')
            self.stderr = "Cannot expect cRNC login prompt."
            self.log("error", self.stderr)
            return False
        self.send_str(self.info.username)

        if self.expect('ENTER PASSWORD < ', std=False) < 0:
            self.log('notice', "failed", head='', tag='r')
            self.stderr = "Cannot expect cRNC login password prompt."
            self.log("error", self.stderr)
            return False
        self.send_str(self.info.password)

        if self.expect(self.prompt) < 0:
            self.log('notice', "failed", head='', tag='r')
            self.stderr = "Cannot expect cRNC prompt after login."
            self.log("error", self.stderr)
            self.connected = False
        else:
            self.log('notice', "successful", head='', tag='g')
            self.connected = True
        return self.connected

    def check_unit(self):
        if not (self.connected or self.connect()):
            return False
        prog = re.compile(r"([A-Z]+)-[0-9].*?WO-EX\s+(?:-|FLTY)")
        units = ['OMU', 'MXU', 'ICSU', 'RSMU', 'DMCU']
        self.chdir("main")
        self.send("ZUSI;")
        work_units = prog.findall(self.stdout)
        for unit in units:
            if unit not in work_units:
                self.stderr = "Unit %s not ready" % unit
                self.log("warn", self.stderr)
                return False

        self.get_unit_info()

        return True

    def get_build_info(self):
        self.send_pom_cmd("type", "/RUNNING/SW_LEVEL.TXT")
        finds = re.findall(r"(\w+) info: (\w+)", self.stdout)
        for (name, info) in finds:
            if name == "Build":
                self.build = info
            elif name == "Corr":
                self.corr = info
        return "%s %s" % (self.build, self.corr)
        
    def get_package(self):
        self.chdir("main")
        self.send("ZWQO:CR;")
        packages = re.findall(r"(\w+).+?Y\s+Y", self.stdout)
        if packages:
            self.package = packages[0]
        return self.package

    def get_unit_info(self):
        # DMCU has no phys, so refind phys
        comp = r"([A-Z]+)-([0-9])\s+([0-9A-F]{4})H\s+\w+\s+WO-EX\s+(?:-|FLTY)"
        prog = re.compile(comp)
        self.unit_info = prog.findall(self.stdout)
        return self.unit_info

    def get_unit_pyhs(self, unit):
        for units in self.unit_info:
            if units[0] == unit:
                return units[2]

    def get_unit_id(self, unit):
        for units in self.unit_info:
            if units[0] == unit:
                return units[1]

    def chdir(self, unit, buf=False):
        """ 
        change the command base unit
        @unit: main|unit|OMU|ICSU
        """
        if unit == "main":
            self.set_prompt("< ")
            if "< " in self.stdout_lastline:
                return True
            while True:
                self.send("ZEEE;", timeout=1.0, regex="< ")
                if self.stderr >= 0:
                    break
                else:
                    time.sleep(1.0)
        else:
            if unit == "unit":
                cmd = "ZDDS;"
            else:
                if unit not in CRNC_UNIT:
                    self.log("warn", "err unit %s in chdir" % unit)
                    return False
                cmd = "ZDDS:%s,%s:%s;" % (unit, self.get_unit_id(unit), 
                                          "BUF" if buf else "")
            self.chdir("main")
            for times in xrange(50):
                self.send(cmd, timeout=2.0, regex=["< ", "MAN>"])
                if self.stderr != 1:
                    time.sleep(1.5*(times+1))
                else:
                    self.set_prompt(">")
                    break
        return True

    def get_timestamp(self):
        return self.timestamp

    def expect(self, regex, timeout=10, std=True):
        """Read until one from a list of a regular expressions matches.
        
        The first argument is a list or a string of regular expressions, 
        either compiled (re.RegexObject instances) or uncompiled (strings).
        The optional second argument is a timeout, in seconds; default
        is no timeout.
        @std: The optional third argument is whether remove the extra prompt line,
        in boolean: default is True
        
        return a number: the index in the list of the
        first regular expression that matches
        """
        
        reg_list = []
        if type(regex) == types.ListType:
            reg_list = regex
        else:
            reg_list.append(regex)
        try:
            expects = self.tn.expect(reg_list, timeout)
        except Exception:
            self.log("exception", "telnet expect failed.")
            self.stderr = -1
            return False

        self.stderr = expects[0]

        msg_lines = []
        for line in remove_unprint_char(expects[2]).splitlines():
            if line.strip():
                msg_lines.append(line)

        self.allout = '\n'.join(msg_lines)
        if len(msg_lines) > 0:
            self.stdout_lastline = msg_lines[-1]

        self.record('\n'.join(msg_lines))

        if std:
            start_line = 1  # remove command line
            end_line = len(msg_lines)
            end_line -= 1   # remove prompt line
            if self.prompt == '< ':
                end_line -= 1  # remove main level extra line
            if end_line < start_line:
                end_line = start_line
            self.stdout = '\n'.join(msg_lines[start_line:end_line])
        else:
            self.stdout = ""

        return self.stderr

    def send_str(self, string, revert=True):
        """Just send a string as a command to cRNC
        """
        if string == "":
            self.record("\n")
        self.timestamp = int(time.time())
        try:
            self.tn.write(string.encode('ascii') + b'\r')
        except Exception:
            self.log("exception", "failed to send command (%s)." % string)
            self.close()
            if revert and self.connect():
                self.send_str(string, False)

    def send(self, cmd, timeout=10, regex=None, std=True, revert=True):
        """ send a command to cRNC and return respond
        timout: command timeout
        regex: expect regex
        std: weather to get respond
        revert: if send command failed, revert connection and re-send
        """
        self.send_str(cmd, revert=revert)
        if regex is not None:
            self.expect(regex, timeout=timeout, std=std)
        else:
            self.expect(self.prompt, timeout=timeout, std=std)
        return self.stdout, self.stderr

    def get(self, remote_file, local_file, msg='file', basename=False):
        basedir = os.path.split(local_file)[0] 
        if not os.path.exists(basedir):
            self.log("debug", "make local dir %s" % basedir)
            os.makedirs(basedir)

        ftp = FTP(self.info.hostname, self.info.username, self.info.password)

        self.log("debug", "ftp connect to %s" % self.info.hostname)
        self.log("debug", "getting file %s --> %s" % (remote_file, local_file))
        show_name = os.path.basename(local_file) if basename else local_file
        self.log('notice', "get %s to %s" % (msg, show_name), end=' ')

        file_handler = open(local_file, "wb")
        try:
            ftp.retrbinary("RETR " + remote_file, file_handler.write)
        except Exception:
            self.log("notice", "failed", head='', tag='r')
            self.log("exception", "get file failed.")
            rslt = False
        else:
            self.log("notice", "successful", head='', tag='g')
            rslt = True
        finally:
            file_handler.close()
            ftp.quit()
            del ftp
            return rslt

    def put(self, local_file, remote_file, msg='file', basename=False):
        if not os.path.isfile(local_file):
            msg = "ftp to put file error due to %s not exist." % local_file
            self.log("error", msg)
            return False

        ftp = FTP(self.info.hostname, self.info.username, self.info.password)

        self.log("debug", "ftp connect to %s" % self.info.hostname)
        self.log("debug", "puttting file %s --> %s" % (local_file, remote_file))
        show_name = os.path.basename(remote_file) if basename else remote_file
        self.log('notice', "put %s to %s" % (msg, show_name), end=' ')

        file_handler = open(local_file, "rb" )
        try:
            ftp.storbinary("STOR " + remote_file, file_handler, 4096)
        except Exception:
            self.log("notice", "failed", head='', tag='r')
            self.log("exception", "put file failed.")
            rslt = False
        else:
            self.log("notice", "successful", head='', tag='g')
            rslt = True
        finally:
            file_handler.close()
            ftp.quit()
            del ftp
            return rslt

    def trace_omu_err(self, cd=True):
        """@cd: if cd omu"""
        if cd:
            self.chdir("OMU")
        self.send("ZGSC:F,,,,E", timeout=120)
        err_codes = re.findall(r"USER DATA :  ([0-9A-F]{8})", self.stdout)
        if err_codes:
            msg = "omu error code found: %s" % ','.join(err_codes)
            self.log("debug", msg)
            return self.decode_error(err_codes[-1], False)
        else:
            self.log("info", "no error code found.")
            return None

    def restart(self):
        self.log('info', "Reboot %s" % self.name, end=' ')
        if self.connected:
            self.chdir("main")
            self.send("ZUSS::TOT:FCD:;", regex="Y/N ?", timeout=2.0, std=False)
            self.send("Y", timeout=1.0)
            self.log('notice', "successful", head='', tag='g')
            self.disconnect()
        else:
            self.log('notice', "failed", head='', tag='r')
            self.log('warn', "RNC not connected")
            

    def decode_error(self, code, cd=True):
        """to decode error code
        @code: a hex code number
        @cd: if cd omu"""

        if cd:
            self.chdir("OMU")

        self.send("ZLP:6,MRS")
        self.send("Z6RH:" + code)
        self.log("error", self.stdout)
        return self.stdout

    def clear_omu_buffer(self, omu=True):
        if omu:
            self.chdir("OMU")
        for times in xrange(50):
            if times > 0:
                time.sleep(1.0)
            self.send("ZOTH")
            if "UNKNOWN COMMAND" in self.stdout or "NO MESSAGE" in self.stdout:
                break

    def send_pom_cmd(self, cmd, param):
        self.chdir("OMU")
        self.send("ZL:P")
        self.send("ZLP:P,POM")
        self.send("ZPS:%s,%s" % (cmd, param))

    def get_date(self):
        self.chdir("main")
        self.send("ZDCD;")
        date = re.findall(r"(\d+-\d+-\d+)\s+(\d+:\d+:\d+)", self.stdout)
        return ' '.join(date[0])

# remove the charactor which not print
def remove_unprint_char(msg):
    is_valid = False
    str_list = []
    for char in msg:
        if is_valid:
            str_list.append(char)
        elif ord(char) > 32 and ord(char) < 127:
            str_list.append(char)
            is_valid = True
    return ''.join(str_list)